﻿import platform, subprocess
import socket,select, time,datetime
from collections import deque
from threading import Thread
import tkinter as tk, tkinter.ttk as ttk, tkinter.messagebox
import ui_data
import serial,serial.tools.list_ports

def get_com() -> list:
    """  获取可用串口列表"""
    SYSTEM_PLATFORM = platform.system()
    ret = []
    for port in  serial.tools.list_ports.comports():
        if(SYSTEM_PLATFORM == "Windows"):
            ret.append("\\\\.\\" + port.name)
        else:
            ret.append(port.name)
    return ret
    pass


def valid_port(port:str)->bool:
    """验证端口的有效性"""
    try:
        port = int(port)
        if( port >= 0 and port < 65535):
            return True
    except:
        return False
    else:
        return False

def get_ip() -> list:
    """ 获取网卡IP地址"""
    UNAME = list(platform.uname())
    ret = []
    if(UNAME[0] == "Windows"):
        sui = subprocess.STARTUPINFO() # 使 subprocess.Popen 调用时不出现一闪而过的窗口
        sui.dwFlags = subprocess.CREATE_NEW_CONSOLE | subprocess.STARTF_USESHOWWINDOW
        sui.wShowWindow = subprocess.SW_HIDE
        ipconfig_process = subprocess.Popen("ipconfig", stdout=subprocess.PIPE, shell=False, startupinfo = sui)
        while ( 1):
            line = ipconfig_process.stdout.readline()
            if( line == b""):
                break
            if(line.startswith(b"   IPv4")):
                s = line.find(b":")
                if(s != -1):
                    line = line[s+2:].rstrip(b" \r\n")
                    ret.append(line)
    elif(UNAME[0] == "Linux"):
        ipconfig_process = subprocess.Popen("ifconfig", stdout=subprocess.PIPE)
    
    return ret
    pass

def get_com() -> list:
    """  获取可用串口列表"""
    SYSTEM_PLATFORM = platform.system()
    ret = []
    for port in  serial.tools.list_ports.comports():
        if(SYSTEM_PLATFORM == "Windows"):
            ret.append("\\\\.\\" + port.name)
        else:
            ret.append(port.name)
    return ret
    pass

def valid_ip(ip:str)->bool:
    """验证IPv4的有效性"""
    try:
        addr = ip.split(".")
        s = 0
        if(len(addr) == 4):
            for a in addr:
                a = int(a)
                if(a >= 0 and a <= 255):
                    continue
                else:
                    s = 1
                    break
            if(s == 0):
                return True
    except:
        return False
    return False

def valid_mip(ip:str)->bool:
    """验证组播地址的有效性"""
    return True

def valid_port(port:int)->bool:
    """验证 端口的有效性"""
    try:
        p = int (port)
    except:
        return False
    if( p >= 0 and p < 65535):
        return True
    return False

def valid_true(num)->bool:
    return True

def thread_net(dqrecv:deque, dqsend:deque):
    """使用 select 函数 进行 多路IO 
        因为 在Windows 下 select 函数只能 接受 网络套接字， 所以，select 要进行10ms级循环，再检查 队列是否有需要添加或删除的套接字
        dqrecv.pop()的数据结构, 即接收数据结构：
            (add,"TCPS"|"TCPC"|"UDP", sock:socket.socket, id )
            (del,"TCPS"|"TCPC"|"UDP", sock:socket.socket, id )
        dqsend.append()的数据结构：
            1. TCPS 收到连接 ("accept", "TCPS", sock_tcps:socket.socket, id,sock_tcpa:socket.socket, addr_tcpa)
            2. 收到报文("accept", "TCP", sock_tcp:socket.socket, id, data)
            3. 对侧关闭("peerclose", "TCP", sock_tcp:socket.socket, id,)
            10. 套接字删除 ("del","TCPS"|"TCPC"|"UDP", sock:socket.socket, id)
    """
    socket_list = {} # 保存 套接字类型 元素类型 为sock: ("TCPS"|"TCPC"|"UDP", id,[1,0,1]) 后面为指示保存在哪个list中
    r_list,w_list,x_list = [],[],[] # for select
    select_list = (r_list,w_list,x_list)
    while True:
        if(dqrecv):
            msg  = dqrecv.pop()
            if(msg):
                if(msg[0] == "add" and  msg[2] not in socket_list):
                    if(msg[1] in ["TCPS", "TCP"]): # TCP 包含 TCPA
                        r_list.append(msg[2])
                        x_list.append(msg[2])
                        socket_list[msg[2]] = (msg[1] , msg[3],[1,0,1])
                    elif(msg[1] == "TCPC"):
                        w_list.append(msg[2])
                        x_list.append(msg[2])
                        socket_list[msg[2]] = ("TCP", msg[3],[0,1,1])
                    elif(msg[1] == "UDP"):
                        r_list.append(msg[2])
                        x_list.append(msg[2])
                        socket_list[msg[2]] = ("UDP", msg[3],[1,0,1])
                elif(msg[0] == "del"):
                    if(msg[2] in socket_list):
                        i = 0
                        for l in socket_list[msg[2]][2]:
                            if(l): 
                                select_list[i].remove(msg[2])
                                socket_list[msg[2]][2][i] = 0
                            i+=1
                        msg[2].close()
                        socket_list.pop(msg[2])
                        dqsend.append(msg) #通知已删除套接字
                    # else:
                    #     dqsend.append(("del_fail", * msg[1:])) #通知已删除套接字
                elif(msg[0]== "quit"):
                    break  
                pass
        if(not (r_list or w_list or  x_list)):
            time.sleep(0.01) # 休息10ms
            continue 
        try:
            rr_list,rw_list,rx_list = select.select(r_list,w_list,x_list, 0.01)
        except Exception as e:
            # select 出错 检查所有 套接字， 关闭对应套接字
            
            # for r in rr_list:
            #     if(r.fileno == -1):
            #         rlist.remove(r)
            #         dqsend.append(("TCPS","err2", r ))
            # for r in rw_list:
            #     if(r.fileno == -1):
            #         rlist.remove(r)
            #         dqsend.append(("TCP","err2", r ))
            # for r in rx_list:
            #     if(r.fileno == -1):
            #         rlist.remove(r)
            #         dqsend.append(("UDP","err2", r ))
            # print (e)
            pass
        for r in rr_list: # 收到连接，或收到报文 或连接成功
            try:
                if(socket_list[r][0] == "TCPS"): #此处仅通知， 加入select 还要主线程发一个命令过来
                    sock_tcp, addr = r.accept()
                    dqsend.append(("accept","TCPS",r, socket_list[r][1], sock_tcp, addr))
                elif(socket_list[r][0] == "TCP"):
                    data = r.recv(4096) # UDP 调用了 connect 所以也用 recv
                    if data:
                        dqsend.append(("recv", "TCP", r, socket_list[r][1], data))
                    else: #收到0字节报文意为对侧关闭
                        if(r in socket_list): # 从 list 中删除 不再 select 这个套接字
                            id =  socket_list[r][1]
                            i = 0
                            for l in socket_list[r][2]:
                                if(l): 
                                    select_list[i].remove(r)
                                    socket_list[r][2][i] = 0
                                i+=1
                            socket_list.pop(r)
                            r.close()
                            dqsend.append(("peerclose","TCP",r, id))
                        pass
                elif(socket_list[r][0] == "UDP"):
                    data,addr = r.recvfrom(4096)
                    dqsend.append(("recvfrom","UDP",  r, socket_list[r][1], data,addr))
            except Exception as e:
                #处理 出错
                continue
        for w in rw_list:
            if(socket_list[w][0] == "TCP"): # TCPC连接成功
                r_list.append(w)
                w_list.remove(w)
                socket_list[w][2][0] = 1
                socket_list[w][2][1] = 0
                dqsend.append(("connect", "TCP", w, socket_list[w][1], )) # 通道 UI 连接成功

        for x in rx_list: # 连接失败 / 发送失败
            if(x in socket_list): # 从 list 中删除 不再 select 这个套接字
                s = (socket_list[x][0],socket_list[x][1],(socket_list[x][2][0],socket_list[x][2][1],socket_list[x][2][2] ))
                i = 0
                for l in socket_list[x][2]:
                    if(l): 
                        select_list[i].remove(x)
                        socket_list[x][2][i] = 0
                    i+=1
                socket_list.pop(x)
                dqsend.append(("err_select_x",s[0], x, s[1], s[2] ))
            
    pass
 
def str_get1(w):
    return w.get()

def int_get1(w):
    return int(w.get())

class CData():
    STATUS_STOP = 0
    STATUS_RUN = 1
    STATUS_CLOSEL = 2
    STATUS_CLOSER = 3
    STATUS_CLOSE = 4
    STATUS_CONNECTING = 5
    STATUS_CONNECTERR = 6
    _types:dict = {}
    def __init__(self, **kwargs) -> None:
        self.info:str = ""
        self.type:str = ""
        self.status:str = ("","#000000")
        self.id:int = None
        self.fd:socket.socket = None
        pass
    
    @staticmethod
    def AddType(t):
        CData._types[t.GetClassName()] = t
        pass
    
    @staticmethod
    def GetTypes():
        return CData._types
    
    def setattr(self, **kwargs):
        for key in kwargs:
            if hasattr(self, key) and not hasattr(getattr(self, key),"__call__"): setattr(self,key, kwargs[key])

    
class TCPS(CData):
    STATUS = {
        CData.STATUS_STOP:    ("未曾启动", "#FF0000", ),
        CData.STATUS_RUN:     ("正常服务", "#00FF00", ),
        CData.STATUS_CLOSE:   ("关闭服务", "#FF8000", ),
    }
    def __init__(self, **kwargs) -> None:
        """内部参数:
        @param[in] _lip: 本地 TCPS 服务地址
        @param[in] _lport: 本地 TCPS 端口
        @param[in] _backlog: 本地 TCPS 最大数量
        """
        super().__init__(**kwargs)
        self.type = self.__class__.__name__
        self._lip:str = str(kwargs["lip"]) if "lip" in kwargs else "0.0.0.0" ##< TCPS地址
        self._lport:int = int(kwargs["lport"]) if "lport" in kwargs else 2404 ##< TCPS端口
        self._backlog:int = int(kwargs["backlog"]) if "backlog" in kwargs else 5 ##< TCPS最大连接数
        self.GUI_data = None
        pass
    
    @staticmethod
    def GetClassName():
        """静态方法 获取 此 class 名 """
        return __class__.__name__
        
    @staticmethod
    def GUI(parent, tcps, **kwargs):
        """TCPS 的 GUI 设置窗口"""
        __ips:list = kwargs["ips"] if "ips" in kwargs else ["0.0.0.0",* get_ip(),"127.0.0.1"] ##< IP 地址列表
        __lport = kwargs["port"] if "port" in kwargs else tcps._lport  ##< 端口 
        __backlog = kwargs["backlog"] if "backlog" in kwargs else tcps._backlog # 最大连接数
        if tcps._lip not in __ips:
            __ips.insert(0, tcps._lip)
        lf = tk.LabelFrame(parent, name="channel", text="TCPS 通道设置", labelanchor="nw")
        tcps.GUI_data = [tk.StringVar(lf, value= tcps._lip), tk.IntVar(lf, value= tcps._lport), tk.IntVar(lf, value= tcps._backlog)]
        tk.Label(lf, text="本地 IP:").grid(column=0,row=1)
        pLip = ttk.Combobox(lf,name = "lip",values=__ips, textvariable = tcps.GUI_data[0])
        pLip.grid(column=1,row=1, padx=2,pady=2)
        # pLip.current(0)
        tk.Label(lf, text="Port:").grid(column=2,row=1)
        pLport = tk.Entry(lf,name = "lport", width=8, textvariable= tcps.GUI_data[1])
        pLport.grid(column=3, row=1, padx=2,pady=2)
        # pLport.delete(0,"end")
        # pLport.insert(0, __lport)
        tk.Label(lf, text="Backlog:").grid(column=4,row=1)
        pBacklog = tk.Entry(lf, name = "backlog", width=8, textvariable= tcps.GUI_data[2])
        pBacklog.grid(column=5, row=1, padx=2,pady=2)
        # pBacklog.delete(0,"end")
        # pBacklog.insert(0, __backlog)
        return (lf,48)
        pass
    def validate(self):
        """检查参数的有效性"""
        return valid_ip(self._lip) and valid_port(self._lport)
    
    def apply(self):
        self._lip = self.GUI_data[0].get()
        self._lport = self.GUI_data[1].get()
        self._backlog = self.GUI_data[2].get()
        delattr(self, "GUI_data")
        self.info = self.type + ":" + self._lip +  ":" + str(self._lport) + " backlog:" + str(self._backlog)
        
    def createfd(self):
        """创建一个 (文件描述符) socket | 句柄"""
        self.fd = socket.socket(socket.AF_INET, socket.SOCK_STREAM ,socket.IPPROTO_TCP)
        self.fd.bind( (self._lip, self._lport))
        self._lip, self.lport = self.fd.getsockname()
        self.status = self.STATUS[self.STATUS_STOP]
        return self.fd    
    
    def start(self, dqsend):
        """启动此通道"""
        try:
            if self.fd == None:
                self.createfd()
            self.fd.listen(self._backlog)
            dqsend.append(("add", self.type, self.fd, self.id,))
        except Exception as e:
            return e
        self.status = self.STATUS[self.STATUS_RUN]
        return 0
    
    def stop(self, dqsend):
        """关闭此通道"""
        if self.status == self.STATUS[self.STATUS_RUN]:
            dqsend.append(("del", self.type, self.fd, self.id,))
        else:
            if(self.fd):
                self.fd.close()
                self.fd = None
                self.status = self.STATUS[self.STATUS_CLOSE] #本地关闭服务
    def recreatefd(self):
        if(self.fd):
            self.fd.close()
            self.createfd( )
    def stoped(self):
        if(self.fd):
            self.fd.close()
        self.fd = None
        self.status = self.STATUS[self.STATUS_CLOSE] #本地关闭服务
        
    
class TCPA(CData):
    STATUS = {
        CData.STATUS_RUN:     ("连接成功", "#00FF00", ),
        CData.STATUS_CLOSEL:  ("本地断开", "#ff0040", ),
        CData.STATUS_CLOSER:  ("对侧断开", "#ff4000", ),
    }
    def __init__(self, id, fd,lip, lport, rip, rport, **kwargs) -> None:
        """内部参数:
        @param[in] _lip: 本地 TCPS 服务地址
        @param[in] _lport: 本地 TCPS 端口
        @param[in] _rip: 远端的 IP 地址
        @param[in] _rport: 远端的 PORT 端口
        """
        super().__init__(**kwargs)
        self.id = id
        self.fd = fd
        self._lip:str = lip
        self._lport:int = lport
        self._rip:str = rip
        self._rport:int = rport
        self.type = self.__class__.__name__
        self.info = self.type + ":L:" + self._lip +  ":" + str(self._lport) + "/R:" + self._rip +  ":" + str(self._rport)
        self.status = self.STATUS[self.STATUS_RUN]
        self.ups = kwargs["ups"] if "ups" in kwargs else None ##> 上级数据
        pass
    
    @staticmethod
    def GetClassName():
        """静态方法 获取 此 class 名 """
        return __class__.__name__
    
    @staticmethod
    def GUI(parent, tcpa, **kwargs):
        """TCPS 的 GUI 显示窗口"""
        lf = tk.LabelFrame(parent, name="channel", text="TCPC 通道设置", labelanchor="nw")
        tk.Label(lf, text="本地 IP:").grid(column=0,row=1)
        pLip = ttk.Combobox(lf,name="lip",values= list(tcpa._lip) , state="disabled")
        pLip.grid(column=1,row=1, padx=2,pady=2)
        pLip.current(0)
        tk.Label(lf, text="Port:").grid(column=2,row=1)
        pLport = tk.Entry(lf,name="lport", width=8, textvariable=tcpa._lport, state="disabled")
        pLport.grid(column=3, row=1, padx=2,pady=2)
        tk.Label(lf, text="远方 IP:").grid(column=0,row=2)
        pRip = ttk.Combobox(lf,name="rip", values=list(tcpa._rip) , state="disabled")
        pRip.grid(column=1,row=2, padx=2,pady=2)
        pRip.current(0)
        tk.Label(lf, text="Port:").grid(column=2, row=2)
        pRport = tk.Entry(lf,name="rport", width=8, textvariable= tcpa._rport, state="disabled")
        pRport.grid(column=3, row=2, padx=2, pady=2)
        return (lf,75)
    def validate(self):
        """检查参数的有效性"""
        return True
    def send(self, buf):
        """发送所有报文, 如果出错返回 Exception """
        try:
            self.fd.sendall(buf)
        except Exception as e:
            #出错处理, 检查已发送多少字节，更新已发送字节数 
            return e
            pass
        return len(buf)

    def stop(self, dqsend):
        """关闭此通道"""
        if self.status == self.STATUS[self.STATUS_RUN]:
            dqsend.append(("del", "TCP", self.fd, self.id,))
        else:
            if(self.fd):
                self.fd.close()
                self.fd = None
                self.status = self.STATUS[self.STATUS_CLOSEL] #本地关闭服务
    
    def stoped(self):
        if(self.fd):
            self.fd.close()
        self.fd = None
        self.status = self.STATUS[self.STATUS_CLOSEL] #本地关闭服务
    def stoped2(self):
        if(self.fd):
            self.fd.close()
        self.fd = None
        self.status = self.STATUS[self.STATUS_CLOSER] #对侧关闭连接  
class TCPC(CData):
    STATUS = {
        CData.STATUS_STOP:      ("未曾连接", "#FF0000", ),
        CData.STATUS_RUN:       ("连接成功", "#00FF00", ),
        CData.STATUS_CLOSEL:    ("本地断开", "#ff0040", ),
        CData.STATUS_CLOSER:    ("对侧断开", "#ff4000", ),
        CData.STATUS_CONNECTING:("正在连接", "#00ff60", ),
        CData.STATUS_CONNECTERR:("连接出错", "#ff6060", ),
    }
    def __init__(self, **kwargs) -> None:
        """内部参数:
        @param[in] _lip: 远方的 TCPS 服务地址
        @param[in] _lport: 远方的  TCPS 端口
        @param[in] _rip: 本地的 IP 地址
        @param[in] _rport: 本地的 PORT 端口
        """
        super().__init__(**kwargs)
        self.type = self.__class__.__name__
        self.status = self.STATUS[self.STATUS_STOP]
        self._lip:str = str(kwargs["lip"]) if "lip" in kwargs else "0.0.0.0"
        self._lport:int = int(kwargs["lport"]) if "lport" in kwargs else 0
        self._rip:str = str(kwargs["rip"]) if "rip" in kwargs else "127.0.0.1"
        self._rport:int = int(kwargs["rport"]) if "rport" in kwargs else 2404
        self.GUI_data = None
        pass

    @staticmethod
    def GetClassName():
        """静态方法 获取 此 class 名 """
        return __class__.__name__

    @staticmethod
    def GUI(parent, tcpc, **kwargs):
        """TCPC 的 GUI 显示窗口"""
        __lips:list = kwargs["ips"] if "ips" in kwargs else ["0.0.0.0",* get_ip(),"127.0.0.1"] ##< TCPC 本地IP 地址列表
        __lport = kwargs["port"] if "port" in kwargs else tcpc._lport  ##< TCPC 本地端口 
        __rips:list = kwargs["ips"] if "ips" in kwargs else ["127.0.0.1", * get_ip()] ##< IP 地址列表
        __rport = kwargs["port"] if "port" in kwargs else tcpc._rport  ##< 端口 
        lf = tk.LabelFrame(parent, name="channel", text="TCPC 通道设置", labelanchor="nw")
        if tcpc._lip not in __lips:
            __lips.insert(0,tcpc._lip)
        if tcpc._rip not in __rips:
            __rips.insert(0,tcpc._rip)
        tcpc.GUI_data = [tk.StringVar(lf, value= __lips[0]), tk.IntVar(lf, value= __lport), 
                        tk.StringVar(lf, value= __rips[0]), tk.IntVar(lf, value= __rport)]
        tk.Label(lf, text="本地 IP:").grid(column=0,row=1)
        pLip = ttk.Combobox(lf,name="lip",values= __lips , textvariable= tcpc.GUI_data[0])
        pLip.grid(column=1,row=1, padx=2,pady=2)
        # pLip.current(0)
        tk.Label(lf, text="Port:").grid(column=2,row=1)
        pLport = tk.Entry(lf,name="lport", width=8, textvariable=tcpc.GUI_data[1])
        pLport.grid(column=3, row=1, padx=2,pady=2)
        # pLport.delete(0,"end")
        # pLport.insert(0, __lport)
        tk.Label(lf, text="远方 IP:").grid(column=0,row=2)
        pRip = ttk.Combobox(lf,name="rip", values=__rips, textvariable= tcpc.GUI_data[2])
        pRip.grid(column=1,row=2, padx=2,pady=2)
        pRip.current(0)
        tk.Label(lf, text="Port:").grid(column=2, row=2)
        pRport = tk.Entry(lf,name="rport", width=8, textvariable= tcpc.GUI_data[3])
        pRport.grid(column=3, row=2, padx=2, pady=2)
        # pRport.delete(0,"end")
        # pRport.insert(0, __rport)
        return (lf,75)
        pass

    def validate(self):
        """检查参数的有效性"""
        return valid_ip(self._lip) and valid_port(self._lport) and valid_ip(self._rip) and valid_port(self._rport)
        pass
    
    def apply(self):
        self._lip = self.GUI_data[0].get()
        self._lport = self.GUI_data[1].get()
        self._rip = self.GUI_data[2].get()
        self._rport = self.GUI_data[3].get()
        delattr(self, "GUI_data")
        self.info = self.type + ":" + self._lip +  ":" + str(self._lport) + " / " + self._rip +  ":" + str(self._rport)
        
    def createfd(self):
        """创建一个 (文件描述符) socket | 句柄"""
        self.fd = socket.socket(socket.AF_INET, socket.SOCK_STREAM ,socket.IPPROTO_TCP)
        self.fd.setblocking(False)
        self.fd.bind( (self._lip, self._lport))
        self._lip, self.lport = self.fd.getsockname()
        self.status = self.STATUS[self.STATUS_STOP]
        return self.fd    
    
    def start(self, dqsend = None):
        """启动此通道"""
        try:
            if self.fd == None:
                self.createfd()
            self.fd.connect((self._rip,self._rport)) # 连接服务端
            dqsend.append(("add", self.type, self.fd, self.id,))
        except BlockingIOError:
            dqsend.append(("add", self.type, self.fd, self.id,))
            pass
        except Exception as e:
            return e
        self.status = self.STATUS[self.STATUS_CONNECTING]
        return 0
    
    def started(self):
        self.status = self.STATUS[self.STATUS_RUN]
    def recreatefd(self):
        if(self.fd):
            self.fd.close()
            self.createfd( )
    def stop(self, dqsend):
        """关闭此通道"""
        if self.status == self.STATUS[self.STATUS_RUN]:
            dqsend.append(("del", self.type, self.fd, self.id,))
        else:
            if(self.fd):
                self.fd.close()
                self.fd = None
                self.status = self.STATUS[self.STATUS_CLOSEL] #本地关闭连接
    
    def stoped(self):
        if(self.fd):
            self.fd.close()
        self.fd = None
        self.status = self.STATUS[self.STATUS_CLOSEL] #本地关闭连接
    def stoped2(self):
        if(self.fd):
            self.fd.close()
        self.fd = None
        self.status = self.STATUS[self.STATUS_CLOSER] #对侧关闭连接  
    def stoped3(self): 
        """连接出错"""
        if(self.fd):
            self.fd.close()
        self.fd = None
        self.status = self.STATUS[self.STATUS_CONNECTERR] # 连接出错 
    def send(self, buf):
        """发送所有报文, 如果出错返回 Exception """
        try:
            self.fd.sendall(buf)
        except Exception as e:
            #出错处理, 检查已发送多少字节，更新已发送字节数 
            return e
            pass
        return len(buf)
        
class UDP(CData):
    STATUS = {
        CData.STATUS_STOP:    ("未曾启动", "#FF0000", ),
        CData.STATUS_RUN:     ("正常运行", "#00FF00", ),
        CData.STATUS_CLOSE:   ("关闭运行", "#FF8000", ),
    }
    def __init__(self, **kwargs) -> None:
        """内部参数:
        @param[in] _lip: 本地的 UDP 地址
        @param[in] _lport: 本地的 UDP  端口
        @param[in] _rip: 远方的 UDP 地址
        @param[in] _rport: 远方的 UDP 端口
        """
        super().__init__(**kwargs)
        self.type = self.__class__.__name__
        self.status = self.STATUS[self.STATUS_STOP]
        self.send_params = {}
        self._lip:str = str(kwargs["lip"]) if "lip" in kwargs else "0.0.0.0"
        self._lport:int = int(kwargs["lport"]) if "lport" in kwargs else 1808
        self._rip:str = str(kwargs["rip"]) if "rip" in kwargs else "255.255.255.255"
        self._rport:int = int(kwargs["rport"]) if "rport" in kwargs else 2404
        self._mip:str = str(kwargs["mip"]) if "mip" in kwargs else "236.8.8.8"
        self._broadcast = False
        self._mutlicast = False
        pass
    def set(self,**kwargs):
        if "lip" in kwargs: self._lip:str = str(kwargs["lip"])
        if "lport" in kwargs: self._lport:int = int(kwargs["lport"]) 
        if "rip" in kwargs: self._rip:str = str(kwargs["rip"]) 
        if "rport" in kwargs: self._rport:int = int(kwargs["rport"]) 
        if "mip" in kwargs: self._mip:str = str(kwargs["mip"]) 
    @staticmethod
    def GetClassName():
        """静态方法 获取 此 class 名 """
        return __class__.__name__
    
    @staticmethod
    def GUI(parent, udp, **kwargs):
        """TCPC 的 GUI 设置及显示窗口"""
        __lips:list = kwargs["ips"] if "ips" in kwargs else ["0.0.0.0",* get_ip(),"127.0.0.1",] ##< TCPC 本地IP 地址列表
        __lport = kwargs["port"] if "port" in kwargs else udp._lport  ##< TCPC 本地端口 
        __rips:list = kwargs["ips"] if "ips" in kwargs else ["255.255.255.255","127.0.0.1", * get_ip(), "236.8.8.8"] ##< IP 地址列表
        __rport = kwargs["port"] if "port" in kwargs else udp._rport  ##< 端口 
        __mips:str = str(kwargs["mip"]) if "mip" in kwargs else ["236.8.8.8","0,0,0,0",  "224.0.0.4", "239.255.255.255"]
        if udp._lip not in __lips:
            __lips.insert(0,udp._lip)
        if udp._rip not in __rips:
            __rips.insert(0,udp._rip)
        if udp._mip not in __mips:
            __mips.insert(0,udp._mip)
        lf = tk.LabelFrame(parent, name="channel", text="UDP 通道设置", labelanchor="nw")
        udp.GUI_data = [tk.StringVar(lf, value= __lips[0]), tk.IntVar(lf, value= __lport), 
                        tk.StringVar(lf, value= __rips[0]), tk.IntVar(lf, value= __rport),
                        tk.StringVar(lf, value= __mips[0])]
        tk.Label(lf, text="本地IP:").grid(column=0,row=1)
        pLip = ttk.Combobox(lf,name="lip",values= __lips, textvariable= udp.GUI_data[0])
        pLip.grid(column=1,row=1, padx=2,pady=2)
        pLip.current(0)
        tk.Label(lf, text="Port:").grid(column=2,row=1)
        pLport = tk.Entry(lf,name="lport", width=8, textvariable= udp.GUI_data[1])
        pLport.grid(column=3, row=1, padx=2,pady=2)
        # pLport.delete(0, "end")
        # pLport.insert(0, __lport)

        tk.Label(lf, text="远方 IP:").grid(column=0,row=2)
        pRip = ttk.Combobox(lf,name="rip",values= __rips, textvariable= udp.GUI_data[2])
        pRip.grid(column=1,row=2, padx=2,pady=2)
        pRip.current(0)
        tk.Label(lf, text="Port:").grid(column=2,row=2)
        pRport = tk.Entry(lf,name="rport", width=8, textvariable= udp.GUI_data[3])
        pRport.grid(column=3, row=2, padx=2,pady=2)
        # pRport.delete(0, "end")
        # pRport.insert(0, __rport)
        tk.Label(lf, text="组播源IP:").grid(column=0,row=4)
        pMip = ttk.Combobox(lf, name="mip",values=__mips, textvariable=udp.GUI_data[4])
        pMip.grid(column=1,row=4, padx=2,pady=2)
        pMip.current(0)
        return (lf, 102)
        # return (lf, 129)

    def validate(self):
        """检查参数的有效性"""
        return True
        pass
    def apply(self):
        """返回数据"""
        self._lip = self.GUI_data[0].get()
        self._lport = self.GUI_data[1].get()
        self._rip = self.GUI_data[2].get()
        self._rport = self.GUI_data[3].get()
        self._mip = self.GUI_data[4].get()
        delattr(self, "GUI_data")
        self.info = self.type + ":" + self._lip +  ":" + str(self._lport) + \
                " / " + self._rip +  ":" + str(self._rport) + \
                (" / 广播发送" if self._broadcast else "") + \
                (("/ 组播 :" + self._mip) if self._mutlicast else "")
        self.send_params={"rip":["str_int",[self._rip, self._rport],"远方IP", self._rip +":"+ str(self._rport)]}

    def createfd(self):
        """创建一个 (文件描述符) socket | 句柄"""
        def ip_in_range(ip:str, r1:str, r2:str):
            def ip_to_int(ip:str):
                try:
                    l = [int(c) for c in ip.split(".")]
                    return l[0] << 24 | l[1] << 16 | l[2]<< 8 | l[3]
                except:
                    return 0xEFFFFFFF
            a = ip_to_int(ip)
            b = ip_to_int(r1)
            c = ip_to_int(r2)
            if b > c: 
                t = b
                b = c
                c = t
            return True if a>=b and a < c else False
        self.fd = socket.socket(socket.AF_INET, socket.SOCK_DGRAM ,socket.IPPROTO_UDP)
        self.fd.bind( (self._lip, self._lport)) # 绑定一个本地的网卡的 IP 地址或 "0.0.0.0" , 并指定端口， 如果为0 那么就由系统指定端口
        self.fd.setblocking(False)
        if( self._rip == "255.255.255.255"): # 当目的地址为 "255.255.255.255" 时 设为广播方式
            self.fd.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) # 设置允许发送广播
            self._broadcast = True
        # 如果 组播地址为一个正常的 组播地址, 那么就加入一个组播地址， 其它UDP 就可以通过 sendto 这个组播地址给 这个套接字发报文了
        if (ip_in_range(self._mip, "224.0.0.4","239.255.255.255")):
            self.fd.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 255) # 组播TTL为255
            self.fd.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP,
                                socket.inet_aton(self._mip) + socket.inet_aton(self._lip)) #绑定 组播地址 及 网卡地址或"0.0.0.0"
            self._mutlicast = True
        self._lip, self._lport = self.fd.getsockname()
        return self.fd    
    def start(self, dqsend = None):
        """启动此通道"""
        try:
            if self.fd == None:
                self.createfd()
            dqsend.append(("add", self.type, self.fd, self.id,))
        except Exception as e:
            return e
        self.status = self.STATUS[self.STATUS_RUN]
        return 0
    def started(self):
        pass
    def recreatefd(self):
        if(self.fd):
            self.fd.close()
            self.createfd( )
    def stop(self, dqsend):
        """关闭此通道"""
        if self.status == self.STATUS[self.STATUS_RUN]:
            dqsend.append(("del", self.type, self.fd, self.id,))
        else:
            if(self.fd):
                self.fd.close()
                self.fd = None
                self.status = self.STATUS[self.STATUS_CLOSE] #本地关闭连接 
    def stoped(self):
        if(self.fd):
            self.fd.close()
        self.fd = None
        self.status = self.STATUS[self.STATUS_CLOSE] #本地关闭连接 
    def send(self, buf, addr):
        """发送所有报文, 如果出错返回 Exception """
        try:
            self.fd.sendto(buf, addr)
        except Exception as e:
            #出错处理, 检查已发送多少字节，更新已发送字节数 
            return e
            pass
        return len(buf)



class COM(CData):
    STATUS = {
        CData.STATUS_STOP:    ("未曾启动", "#FF0000", ),
        CData.STATUS_RUN:     ("正常运行", "#00FF00", ),
        CData.STATUS_CLOSE:   ("关闭运行", "#FF8000", ),
    }
    def __init__(self, dqrecv:deque, **kwargs) -> None:
        super().__init__(**kwargs)
        self.type = self.__class__.__name__
        comlist = get_com()
        if(comlist): 
            com = comlist[-1] 
        else: 
            com = ""
        self._com:str = str(kwargs["com"]) if "com" in kwargs else com
        self._baud:str = str(kwargs["baud"]) if "baud" in kwargs else 9600
        self._parity:str = str(kwargs["parity"]) if "parity" in kwargs else "None"
        self._bytesize:str = str(kwargs["bytesize"]) if "bytesize" in kwargs else 8
        self._stopbits:str =  str(kwargs["stopbits"]) if "stopbits" in kwargs else 1
        self.thread = None
        self.dqrecv:deque = dqrecv
        self.GUI_data = None
        self.status = self.STATUS[self.STATUS_STOP]
        pass
    
    @staticmethod
    def GetClassName():
        """静态方法 获取 此 class 名 """
        return __class__.__name__
    
    @staticmethod
    def GUI(parent, com, **kwargs):
        """COM 的 GUI 设置及显示窗口"""
        __com:str = str(kwargs["com"]) if "com" in kwargs else get_com()
        __baud:str = str(kwargs["baud"]) if "baud" in kwargs else [300, 600, 1200, 1800, 2400, 4800, 9600, 19200, 38400, 57600, 115200]
        __parity:str = str(kwargs["parity"]) if "parity" in kwargs else ["None","Even","Odd","Mark","Space"]
        __bytesize:str = str(kwargs["bytesize"]) if "bytesize" in kwargs else [5, 6, 7, 8]
        __stopbits:str =  str(kwargs["stopbits"]) if "stopbits" in kwargs else [1, 1.5, 2]
        lf = tk.LabelFrame(parent, name="channel", text="串口 通道设置", labelanchor="nw")
        com.GUI_data = [tk.StringVar(lf, value= com._com), tk.IntVar(lf, value= com._baud), 
                        tk.StringVar(lf, value= com._parity), tk.IntVar(lf, value= com._bytesize),
                        tk.DoubleVar(lf, value= com._stopbits)]
        # tk.Label(master, text="串口设置:").grid(column=0,row=0, padx=2,pady=2,sticky="n")
        tk.Label(lf, text="串口号:").grid(column=0,row=1)
        pCom = ttk.Combobox(lf,name = "com",values= __com, width=26, textvariable= com.GUI_data[0])
        pCom .grid(column=1,row=1,columnspan=3)
        tk.Label(lf, text="波特率:").grid(column=0,row=2)
        pBaud = ttk.Combobox(lf, name = "baud", width=8,values=__baud, textvariable= com.GUI_data[1])
        pBaud.grid(column=1,row=2)
        tk.Label(lf, text="校验位:").grid(column=2,row=2)
        pParity = ttk.Combobox(lf, name = "parity", width=8,values=__parity, textvariable= com.GUI_data[2])
        pParity.grid(column=3,row=2)
        tk.Label(lf, text="数据位:").grid(column=0,row=3)
        pBytesize = ttk.Combobox(lf, name = "bytesize", width=8,values=__bytesize, textvariable= com.GUI_data[3])
        pBytesize.grid(column=1,row=3)
        tk.Label(lf, text="停止位:").grid(column=2,row=3)
        pStopbits = ttk.Combobox(lf, name = "stopbits", width=8,values=__stopbits, textvariable= com.GUI_data[4])
        pStopbits.grid(column=3,row=3)
        if com._com in __com: pCom.current(__com.index(com._com))
        if com._baud in __baud: pBaud.current(__baud.index(com._baud))
        if com._parity in __parity: pParity.current(__parity.index(com._parity))
        if com._bytesize in __bytesize: pBytesize.current(__bytesize.index(com._bytesize))
        if com._stopbits in __stopbits: pStopbits.current(__stopbits.index(com._stopbits))
        # print(tk.Misc.winfo_height(master))
        return (lf, 90)
        # master.master.paneconfig(master, sticky="nwes", height= 90)#117)

    def validate(self):
        """检查参数的有效性"""
        return True
        pass
    def apply(self):
        """返回数据"""
        self._com = self.GUI_data[0].get()
        self._baud = self.GUI_data[1].get()
        self._parity = self.GUI_data[2].get()
        self._bytesize = self.GUI_data[3].get()
        self._stopbits = self.GUI_data[4].get()
        delattr(self, "GUI_data")
        self.info = self.type + ":" + self._com +  "," + str(self._baud) + \
                "," + self._parity +  "," + str(self._bytesize) + "," + str(self._stopbits)

    def createfd(self):
        """创建一个 (文件描述符) | (串口句柄)"""
        try:
            self.fd:serial.Serial = serial.Serial(port= self._com,
                                                    baudrate= self._baud, 
                                                    bytesize=self._bytesize, 
                                                    parity=self._parity[0] , 
                                                    stopbits=self._stopbits,
                                                    timeout = 0.5 )
        except Exception as e:
            return e
        return self.fd    
    def start(self):
        """启动此通道"""
        try:
            if self.fd == None:
                self.createfd()
            self.thread = Thread(target=  COM.__thread_com_io, args=(self.fd, self.id, self.dqrecv,)) 
            self.thread.setDaemon(True)
            self.thread.start()
        except Exception as e:
            return e
        self.status = self.STATUS[self.STATUS_RUN]
        return 0
    def started(self):
        pass
    def recreatefd(self):
        if(self.fd):
            self.fd.close()
            self.createfd( )
    def stop(self, dqsend):
        """关闭此通道"""
        if(self.fd): # 直接关闭，线程会异常退出
            self.fd.close()
            self.fd = None
            self.status = self.STATUS[self.STATUS_CLOSE] #本地关闭连接 
    def stoped(self):
        if(self.fd):
            self.fd.close()
        self.fd = None
        self.status = self.STATUS[self.STATUS_CLOSE] #本地关闭连接 
    def send(self, buf):
        """发送所有报文, 如果出错返回 Exception """
        try:
            self.fd.write(buf )
            # self.fd.flush()
        except Exception as e:
            #出错处理, 检查已发送多少字节，更新已发送字节数 
            return e
            pass
        return len(buf)   
    @staticmethod
    def __thread_com_io(fd:serial.Serial, id, dqsend:deque):
        try:
            while True:
                if fd.in_waiting:
                    time.sleep(0.05) # 串口自环时老会收不全，所以在这个地方等一下再接收
                    l =  fd.read(fd.in_waiting)
                    if l:
                        dqsend.append(("read", "COM", fd, id, l))
                else:
                    time.sleep(0.01)
        except serial.serialutil.SerialException as e:
            dqsend.append(("exit", "COM", fd, id, e))
            return
        except Exception as e:
            dqsend.append(("err_com", "COM", fd, id, e))
            return
        pass

    
def create_thread_net(dqrecv,dqsend):
    """创建一个线程 for 网络传输"""
    pthread_net = Thread(target=thread_net, args=( dqsend,  dqrecv)) # 创建线程 dqrecv / dqsend 需要反过来传送
    pthread_net.setDaemon(True)
    pthread_net.start()
    return pthread_net


 
if __name__ == '__main__':
    pass